Micron Document
C.S.Burner 🪙 ━►🔥GIT Node

Commit 4dfec0a127548cf3c7e17fbd40855dafaa0c2b0a


Parents : da3df4c
Author : Ivan <e318cbc04468bd574db2b4523dddd710>
Signature : T66BB85Valid, signed by author
Date : 2026-09-12T06:22:19-05:00

refactor(map): extract map ping modal state into useMapPing

Move the ping modal cluster (modal visibility, destination hash, lat/lon,
zoom, summary computed, open/send actions) into js/map/useMapPing.js bound
via setup() merge; $t and the layers tag helper arrive via options.

Changes
Diff

diff --git a/meshchatx/src/frontend/components/map/MapPage.vue b/meshchatx/src/frontend/components/map/MapPage.vue
index c410dd9e..3ed91ed4 100644
--- a/meshchatx/src/frontend/components/map/MapPage.vue
+++ b/meshchatx/src/frontend/components/map/MapPage.vue
@@ -1077,7 +1077,7 @@
<script>
import { useConfigStore } from "../../js/stores/configStore.js";
-import { markRaw } from "vue";
+import { getCurrentInstance, markRaw } from "vue";
import "ol/ol.css";
import "../../js/mapVectorWebFonts.js";
import { apply as applyMapboxStyle } from "ol-mapbox-style";
@@ -1202,6 +1202,7 @@ import { styleFromMcxProperties } from "../../js/mapExchange/styleFromProperties
import { computeSegmentMetrics, buildBearingOverlayHtml, buildBearingLiveTooltipHtml } from "../../js/mapGeodesy.js";
import { isLocalMapServiceUrl } from "../../js/mapLocalUrl.js";
import * as lxmfApi from "../../js/api/lxmf.js";
+import { useMapPing } from "../../js/map/useMapPing.js";
const OPENFREEMAP_DEFAULT_STYLE = "https://tiles.openfreemap.org/styles/bright";
const DEFAULT_OSM_RASTER = DEFAULT_TILE_SERVER_URL;
@@ -1255,6 +1256,15 @@ export default {
},
},
emits: ["update-title"],
+ setup() {
+ const inst = getCurrentInstance();
+ return {
+ ...useMapPing({
+ t: (key) => inst?.proxy.$t(key),
+ getLayers: () => inst?.proxy.layersTagForShare(),
+ }),
+ };
+ },
data() {
return {
map: null,
@@ -1405,12 +1415,6 @@ export default {
contextMenuFeature: null,
contextMenuCoord: null,
- showMapPingModal: false,
- pingDestinationHash: "",
- mapPingLat: 0,
- mapPingLon: 0,
- mapPingZoom: 10,
-
exportRegionPresets: [
{ id: "world", bbox: WORLD_MBTILES_BBOX.slice(), minZoom: 0, maxZoom: 4 },
{ id: "europe", bbox: [-12, 35, 40, 72], minZoom: 0, maxZoom: 10 },
@@ -1549,9 +1553,6 @@ export default {
out.sort((a, b) => a.label.localeCompare(b.label));
return out;
},
- mapPingSummary() {
- return `${this.mapPingLat.toFixed(6)}, ${this.mapPingLon.toFixed(6)} @ z${Math.round(this.mapPingZoom)}`;
- },
drawFeatureDescriptionSanitized() {
const p = this.drawFeatureInfoPayload;
if (!p || !p.description?.trim() || !p.descriptionIsHtml) {
@@ -2494,42 +2495,6 @@ export default {
this.pingDestinationHash = "";
this.showMapPingModal = true;
},
- openPingModalAt(lat, lon, zoom) {
- this.mapPingLat = lat;
- this.mapPingLon = lon;
- this.mapPingZoom = zoom;
- this.pingDestinationHash = "";
- this.showMapPingModal = true;
- },
- async sendMapPing() {
- const hash = (this.pingDestinationHash || "").trim();
- if (!hash || hash.length !== 32) {
- ToastUtils.error(this.$t("map.ping_invalid_destination"));
- return;
- }
- const layers = this.layersTagForShare();
- const uri = buildMeshchatMapUri({
- lat: this.mapPingLat,
- lon: this.mapPingLon,
- zoom: this.mapPingZoom,
- layers,
- label: "Ping",
- });
- const content = `${this.$t("map.ping_message_prefix")} ${uri}`;
- try {
- await window.api.post(apiPath("/lxmf-messages/send"), {
- lxmf_message: {
- destination_hash: hash,
- content,
- },
- });
- ToastUtils.success(this.$t("map.ping_sent"));
- this.showMapPingModal = false;
- } catch (e) {
- console.error(e);
- ToastUtils.error(this.$t("map.ping_failed"));
- }
- },
isLocalUrl(url) {
return isLocalMapServiceUrl(url, typeof window !== "undefined" ? window.location.origin : "");
},

diff --git a/meshchatx/src/frontend/js/map/useMapPing.js b/meshchatx/src/frontend/js/map/useMapPing.js
new file mode 100644
index 00000000..578bbad9
--- /dev/null
+++ b/meshchatx/src/frontend/js/map/useMapPing.js
@@ -0,0 +1,78 @@
+// @ts-check
+
+import { computed, ref } from "vue";
+
+import ToastUtils from "../ToastUtils.js";
+import { apiPath } from "../constants.js";
+import { buildMeshchatMapUri } from "../mapLinkUtils.js";
+
+/**
+ * Map ping modal state for MapPage: target coordinates, destination hash
+ * selection, the summary label, and the send action.
+ *
+ * options.t translates i18n keys (the host passes its $t) and
+ * options.getLayers returns the share layers tag (the host passes
+ * layersTagForShare) so the composable stays free of instance-only APIs.
+ */
+export function useMapPing(options = {}) {
+ const { t = (key) => key, getLayers = () => "" } = options;
+
+ const showMapPingModal = ref(false);
+ const pingDestinationHash = ref("");
+ const mapPingLat = ref(0);
+ const mapPingLon = ref(0);
+ const mapPingZoom = ref(10);
+
+ const mapPingSummary = computed(() => {
+ return `${mapPingLat.value.toFixed(6)}, ${mapPingLon.value.toFixed(6)} @ z${Math.round(mapPingZoom.value)}`;
+ });
+
+ function openPingModalAt(lat, lon, zoom) {
+ mapPingLat.value = lat;
+ mapPingLon.value = lon;
+ mapPingZoom.value = zoom;
+ pingDestinationHash.value = "";
+ showMapPingModal.value = true;
+ }
+
+ async function sendMapPing() {
+ const hash = (pingDestinationHash.value || "").trim();
+ if (!hash || hash.length !== 32) {
+ ToastUtils.error(t("map.ping_invalid_destination"));
+ return;
+ }
+ const layers = getLayers();
+ const uri = buildMeshchatMapUri({
+ lat: mapPingLat.value,
+ lon: mapPingLon.value,
+ zoom: mapPingZoom.value,
+ layers,
+ label: "Ping",
+ });
+ const content = `${t("map.ping_message_prefix")} ${uri}`;
+ try {
+ await window.api.post(apiPath("/lxmf-messages/send"), {
+ lxmf_message: {
+ destination_hash: hash,
+ content,
+ },
+ });
+ ToastUtils.success(t("map.ping_sent"));
+ showMapPingModal.value = false;
+ } catch (e) {
+ console.error(e);
+ ToastUtils.error(t("map.ping_failed"));
+ }
+ }
+
+ return {
+ showMapPingModal,
+ pingDestinationHash,
+ mapPingLat,
+ mapPingLon,
+ mapPingZoom,
+ mapPingSummary,
+ openPingModalAt,
+ sendMapPing,
+ };
+}

diff --git a/tests/frontend/useMapPing.test.js b/tests/frontend/useMapPing.test.js
new file mode 100644
index 00000000..1fd085cd
--- /dev/null
+++ b/tests/frontend/useMapPing.test.js
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: 0BSD
+
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { useMapPing } from "../../meshchatx/src/frontend/js/map/useMapPing.js";
+import ToastUtils from "../../meshchatx/src/frontend/js/ToastUtils";
+
+vi.mock("../../meshchatx/src/frontend/js/ToastUtils", () => ({
+ default: {
+ success: vi.fn(),
+ error: vi.fn(),
+ warning: vi.fn(),
+ info: vi.fn(),
+ },
+}));
+
+describe("useMapPing", () => {
+ beforeEach(() => {
+ window.api = {
+ post: vi.fn().mockResolvedValue({ data: {} }),
+ };
+ });
+
+ it("openPingModalAt sets the target and opens the modal", () => {
+ const ping = useMapPing();
+ ping.pingDestinationHash.value = "abc";
+ ping.openPingModalAt(52.5, 13.4, 12.6);
+ expect(ping.mapPingLat.value).toBe(52.5);
+ expect(ping.mapPingLon.value).toBe(13.4);
+ expect(ping.mapPingZoom.value).toBe(12.6);
+ expect(ping.pingDestinationHash.value).toBe("");
+ expect(ping.showMapPingModal.value).toBe(true);
+ });
+
+ it("mapPingSummary formats lat lon and rounded zoom", () => {
+ const ping = useMapPing();
+ ping.openPingModalAt(1.23456789, 2.34567891, 12.6);
+ expect(ping.mapPingSummary.value).toBe("1.234568, 2.345679 @ z13");
+ });
+
+ it("sendMapPing rejects an empty destination hash", async () => {
+ const t = vi.fn((key) => key);
+ const ping = useMapPing({ t });
+ await ping.sendMapPing();
+ expect(window.api.post).not.toHaveBeenCalled();
+ expect(t).toHaveBeenCalledWith("map.ping_invalid_destination");
+ expect(ToastUtils.error).toHaveBeenCalledWith("map.ping_invalid_destination");
+ });
+
+ it("sendMapPing rejects a destination hash of the wrong length", async () => {
+ const ping = useMapPing();
+ ping.pingDestinationHash.value = "tooshort";
+ await ping.sendMapPing();
+ expect(window.api.post).not.toHaveBeenCalled();
+ expect(ToastUtils.error).toHaveBeenCalledWith("map.ping_invalid_destination");
+ });
+
+ it("sendMapPing posts a meshchat map uri and closes the modal", async () => {
+ const t = vi.fn((key) => key);
+ const ping = useMapPing({ t, getLayers: () => "discovered" });
+ ping.openPingModalAt(52.5, 13.4, 11);
+ ping.pingDestinationHash.value = "a".repeat(32);
+
+ await ping.sendMapPing();
+
+ expect(window.api.post).toHaveBeenCalledTimes(1);
+ const [path, body] = window.api.post.mock.calls[0];
+ expect(path).toContain("/lxmf-messages/send");
+ expect(body.lxmf_message.destination_hash).toBe("a".repeat(32));
+ expect(body.lxmf_message.content).toContain("map.ping_message_prefix");
+ expect(body.lxmf_message.content).toContain("meshchatx://map?");
+ expect(body.lxmf_message.content).toContain("lat=52.5");
+ expect(body.lxmf_message.content).toContain("lon=13.4");
+ expect(body.lxmf_message.content).toContain("z=11");
+ expect(body.lxmf_message.content).toContain("layers=discovered");
+ expect(body.lxmf_message.content).toContain("label=Ping");
+ expect(ToastUtils.success).toHaveBeenCalledWith("map.ping_sent");
+ expect(ping.showMapPingModal.value).toBe(false);
+ });
+
+ it("sendMapPing keeps the modal open when the request fails", async () => {
+ window.api.post = vi.fn().mockRejectedValue(new Error("boom"));
+ const ping = useMapPing({ t: (key) => key });
+ ping.openPingModalAt(52.5, 13.4, 11);
+ ping.pingDestinationHash.value = "b".repeat(32);
+
+ await ping.sendMapPing();
+
+ expect(ToastUtils.error).toHaveBeenCalledWith("map.ping_failed");
+ expect(ping.showMapPingModal.value).toBe(true);
+ });
+});

Served by rngit 1.5.4 - Generated in 0.03s